﻿using Extended.WPF.Core.Helpers;
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace Extented.UI.Core.Native
{
    public abstract class FrameworkRenderElement
    {
        static readonly object locker = new object();
        public static double DpiScaleX
        {
            get { return ScreenHelper.ScaleX; }
        }
        public static double DpiScaleY
        {
            get { return DpiScaleX; }
        }
        protected static double RoundLayoutValue(double value, double dpiScale)
        {
            double d = value;
            if (!dpiScale.AreClose(1.0))
            {
                d = Math.Round(value * dpiScale) / dpiScale;
                if (double.IsNaN(d) || double.IsInfinity(d) || d.AreClose(double.MaxValue))
                    d = value;
            }
            else
                d = Math.Round(d);
            return d;
        }
        protected static Size RoundLayoutSize(Size size, double dpiScaleX, double dpiScaleY)
        {
            return new Size(RoundLayoutValue(size.Width, dpiScaleX), RoundLayoutValue(size.Height, dpiScaleY));
        }
        protected static Rect RoundLayoutRect(Rect rect, double dpiScaleX, double dpiScaleY)
        {
            return new Rect(RoundLayoutValue(rect.X, dpiScaleX), RoundLayoutValue(rect.Y, dpiScaleY), RoundLayoutValue(rect.Width, dpiScaleX), RoundLayoutValue(rect.Height, dpiScaleY));
        }
        #region internal classes
        struct MinMax
        {
            public readonly double MinWidth;
            public readonly double MaxWidth;
            public readonly double MinHeight;
            public readonly double MaxHeight;
            internal MinMax(FrameworkRenderElement e)
            {
                MaxHeight = e.MaxHeight;
                MinHeight = e.MinHeight;
                double height = e.Height;
                MaxHeight = Math.Max(Math.Min(double.IsNaN(height) ? double.PositiveInfinity : height, MaxHeight), MinHeight);
                MinHeight = Math.Max(Math.Min(MaxHeight, double.IsNaN(height) ? 0.0 : height), MinHeight);
                MaxWidth = e.MaxWidth;
                MinWidth = e.MinWidth;
                double width = e.Width;
                MaxWidth = Math.Max(Math.Min(double.IsNaN(width) ? double.PositiveInfinity : width, MaxWidth), MinWidth);
                MinWidth = Math.Max(Math.Min(MaxWidth, double.IsNaN(width) ? 0.0 : width), MinWidth);
            }
        }
        #endregion
        bool useLayoutRounding;
        double width;
        double height;
        double minWidth;
        double maxWidth;
        double minHeight;
        double maxHeight;
        Thickness margin;
        string name;
        HorizontalAlignment ha;
        VerticalAlignment va;
        Visibility vi;
        Dock? dock;
        FlowDirection? fd;
        double opacity = 1d;
        bool showBounds;
        bool shouldCalcDpiAwareThickness = true;
        Brush foreground;
        bool clipToBounds;
        bool contentSpecificClipToBounds;
        public FrameworkRenderElement Parent { get; internal set; }
        public Brush Foreground
        {
            get { return foreground; }
            set { SetProperty(ref foreground, value); }
        }
        public FlowDirection? FlowDirection
        {
            get { return fd; }
            set { SetProperty(ref fd, value); }
        }
        public bool ShowBounds
        {
            get { return showBounds; }
            set { SetProperty(ref showBounds, value); }
        }
        public bool UseLayoutRounding
        {
            get { return useLayoutRounding; }
            set { SetProperty(ref useLayoutRounding, value); }
        }
        public bool ClipToBounds
        {
            get { return clipToBounds; }
            set { SetProperty(ref clipToBounds, value); }
        }
        public bool ContentSpecificClipToBounds
        {
            get { return contentSpecificClipToBounds; }
            set { SetProperty(ref contentSpecificClipToBounds, value); }
        }
        public double Opacity
        {
            get { return opacity; }
            set { SetProperty(ref opacity, Math.Max(Math.Min(value, 1d), 0d)); }
        }
        public double Width
        {
            get { return width; }
            set { SetProperty(ref width, value); }
        }
        public double Height
        {
            get { return height; }
            set { SetProperty(ref height, value); }
        }
        public double MinWidth
        {
            get { return minWidth; }
            set { SetProperty(ref minWidth, value); }
        }
        public double MaxWidth
        {
            get { return maxWidth; }
            set { SetProperty(ref maxWidth, value); }
        }
        public double MinHeight
        {
            get { return minHeight; }
            set { SetProperty(ref minHeight, value); }
        }
        public double MaxHeight
        {
            get { return maxHeight; }
            set { SetProperty(ref maxHeight, value); }
        }
        public Thickness Margin
        {
            get { return margin; }
            set { SetProperty(ref margin, value); }
        }
        public string Name
        {
            get { return name; }
            set { SetProperty(ref name, value); }
        }
        public HorizontalAlignment HorizontalAlignment
        {
            get { return ha; }
            set { SetProperty(ref ha, value); }
        }
        public VerticalAlignment VerticalAlignment
        {
            get { return va; }
            set { SetProperty(ref va, value); }
        }
        public Visibility Visibility
        {
            get { return vi; }
            set { SetProperty(ref vi, value); }
        }
        public Dock? Dock
        {
            get { return dock; }
            set { SetProperty(ref dock, value); }
        }
        public bool ShouldCalcDpiAwareThickness
        {
            get { return shouldCalcDpiAwareThickness; }
            set { SetProperty(ref shouldCalcDpiAwareThickness, value); }
        }
        public string ThemeInfo { get; set; }
        bool isFreezed;
        protected FrameworkRenderElement()
        {
            Width = double.NaN;
            Height = double.NaN;
            MaxWidth = double.PositiveInfinity;
            MaxHeight = double.PositiveInfinity;
            HorizontalAlignment = HorizontalAlignment.Stretch;
            VerticalAlignment = VerticalAlignment.Stretch;
            UseLayoutRounding = true;
        }
        public void Measure(Size availableSize, FrameworkRenderElementContext context)
        {
            ApplyTemplate(context);
            MeasureCore(availableSize, context);
            if (double.IsInfinity(context.DesiredSize.Width) || double.IsInfinity(context.DesiredSize.Height))
                throw new ArgumentException("desiredSize has infinity member");
        }
        void MeasureCore(Size availableSize, FrameworkRenderElementContext context)
        {
            var visibility = CalcVisibility(context);
            if (visibility == Visibility.Collapsed)
            {
                context.DesiredSize = new Size(0, 0);
                return;
            }
            Thickness margin = CalcDpiAwareThickness(context.Margin ?? Margin);
            double leftRight = margin.Left + margin.Right;
            double topBottom = margin.Top + margin.Bottom;
            Size size = new Size(Math.Max(availableSize.Width - leftRight, 0d), Math.Max(availableSize.Height - topBottom, 0));
            MinMax minMax = new MinMax(this);
            size.Width = Math.Max(minMax.MinWidth, Math.Min(size.Width, minMax.MaxWidth));
            size.Height = Math.Max(minMax.MinHeight, Math.Min(size.Height, minMax.MaxHeight));
            if (UseLayoutRounding)
                size = RoundLayoutSize(size, DpiScaleX, DpiScaleY);
            Size desiredSize = MeasureOverride(size, context);
            desiredSize = new Size(Math.Max(desiredSize.Width, minMax.MinWidth), Math.Max(desiredSize.Height, minMax.MinHeight));
            if (desiredSize.Width > minMax.MaxWidth)
                desiredSize.Width = minMax.MaxWidth;
            if (desiredSize.Height > minMax.MaxHeight)
                desiredSize.Height = minMax.MaxHeight;
            double widthCandidate = Math.Min(availableSize.Width, desiredSize.Width + leftRight);
            double heightCandidate = Math.Min(availableSize.Height, desiredSize.Height + topBottom);
            if (UseLayoutRounding)
            {
                widthCandidate = RoundLayoutValue(widthCandidate, DpiScaleX);
                heightCandidate = RoundLayoutValue(heightCandidate, DpiScaleY);
            }
            desiredSize = new Size(Math.Max(0d, widthCandidate), Math.Max(0d, heightCandidate));
            context.DesiredSize = desiredSize;
        }
        protected Thickness CalcDpiAwareThickness(Thickness thickness)
        {
            return ShouldCalcDpiAwareThickness ? thickness.Multiply(1d / ScreenHelper.ScaleX) : thickness;
        }
        public void ApplyTemplate(FrameworkRenderElementContext context)
        {
            var visibility = context.Visibility ?? Visibility;
            if (visibility == Visibility.Collapsed)
                return;
            PreApplyTemplate(context);
        }
        protected virtual void PreApplyTemplate(FrameworkRenderElementContext context)
        {
            context.PreApplyTemplate();
        }
        public void Arrange(Rect finalRect, FrameworkRenderElementContext context)
        {
            ArrangeCore(finalRect, context);
        }
        void ArrangeCore(Rect finalRect, FrameworkRenderElementContext context)
        {
            var visibility = CalcVisibility(context);
            if (visibility == Visibility.Collapsed)
            {
                context.RenderSize = new Size(0, 0);
                context.VisualOffset = new Vector(0, 0);
                return;
            }
            context.NeedsClipBounds = ContentSpecificClipToBounds;
            HorizontalAlignment horizontalAlignment = context.HorizontalAlignment ?? HorizontalAlignment;
            VerticalAlignment verticalAlignment = context.VerticalAlignment ?? VerticalAlignment;
            Size arrangeSize = finalRect.Size;
            Thickness margin = CalcDpiAwareThickness(context.Margin ?? Margin);
            double marginWidth = margin.Left + margin.Right;
            double marginHeight = margin.Top + margin.Bottom;
            arrangeSize.Width = Math.Max(0.0, arrangeSize.Width - marginWidth);
            arrangeSize.Height = Math.Max(0.0, arrangeSize.Height - marginHeight);
            Size unclippedDesiredSize = new Size(Math.Max(0d, context.DesiredSize.Width - marginWidth), Math.Max(0d, context.DesiredSize.Height - marginHeight));
            if (arrangeSize.Width.LessThan(unclippedDesiredSize.Width))
            {
                context.NeedsClipBounds = true;
                arrangeSize.Width = unclippedDesiredSize.Width;
            }
            if (arrangeSize.Height.LessThan(unclippedDesiredSize.Height))
            {
                context.NeedsClipBounds = true;
                arrangeSize.Height = unclippedDesiredSize.Height;
            }
            if (horizontalAlignment != HorizontalAlignment.Stretch)
                arrangeSize.Width = unclippedDesiredSize.Width;
            if (verticalAlignment != VerticalAlignment.Stretch)
                arrangeSize.Height = unclippedDesiredSize.Height;
            MinMax minMax = new MinMax(this);
            double effectiveMaxWidth = Math.Max(unclippedDesiredSize.Width, minMax.MaxWidth);
            if (effectiveMaxWidth.LessThan(arrangeSize.Width))
            {
                context.NeedsClipBounds = true;
                arrangeSize.Width = effectiveMaxWidth;
            }
            double effectiveMaxHeight = Math.Max(unclippedDesiredSize.Height, minMax.MaxHeight);
            if (effectiveMaxHeight.LessThan(arrangeSize.Height))
            {
                context.NeedsClipBounds = true;
                arrangeSize.Height = effectiveMaxHeight;
            }
            if (UseLayoutRounding)
                arrangeSize = RoundLayoutSize(arrangeSize, DpiScaleX, DpiScaleY);
            Size oldRenderSize = context.RenderSize;
            if (oldRenderSize != arrangeSize)
                context.ResetSizeSpecificCaches();
            Size innerInkSize = ArrangeOverride(arrangeSize, context);
            context.RenderSize = innerInkSize;
            if (UseLayoutRounding)
                context.RenderSize = RoundLayoutSize(innerInkSize, DpiScaleX, DpiScaleY);
            Size clippedInkSize = new Size(Math.Min(innerInkSize.Width, minMax.MaxWidth), Math.Min(innerInkSize.Height, minMax.MaxHeight));
            if (UseLayoutRounding)
                clippedInkSize = RoundLayoutSize(arrangeSize, DpiScaleX, DpiScaleY);
            context.NeedsClipBounds |= clippedInkSize.Width.LessThan(innerInkSize.Width) || clippedInkSize.Height.LessThan(innerInkSize.Height);
            Size clientSize = new Size(Math.Max(0.0, finalRect.Width - marginWidth), Math.Max(0.0, finalRect.Height - marginHeight));
            if (UseLayoutRounding)
            {
                clientSize = RoundLayoutSize(clientSize, DpiScaleX, DpiScaleY);
            }
            context.NeedsClipBounds |= clientSize.Width.LessThan(clippedInkSize.Width) || clientSize.Height.LessThan(clippedInkSize.Height);
            Vector alignmentOffset = this.ComputeAlignmentOffset(clientSize, clippedInkSize, horizontalAlignment, verticalAlignment);
            if (UseLayoutRounding)
            {
                alignmentOffset.X = RoundLayoutValue(alignmentOffset.X + finalRect.X + margin.Left, DpiScaleX);
                alignmentOffset.Y = RoundLayoutValue(alignmentOffset.Y + finalRect.Y + margin.Top, DpiScaleY);
            }
            else
            {
                alignmentOffset.X += finalRect.X + margin.Left;
                alignmentOffset.Y += finalRect.Y + margin.Top;
            }
            context.VisualOffset = alignmentOffset;
            context.LayoutClip = GetLayoutClip(finalRect.Size, context);
        }
        Vector ComputeAlignmentOffset(Size clientSize, Size inkSize, HorizontalAlignment horizontalAlignment, VerticalAlignment verticalAlignment)
        {
            Vector vector = new Vector();
            if (horizontalAlignment == HorizontalAlignment.Stretch && inkSize.Width > clientSize.Width)
                horizontalAlignment = HorizontalAlignment.Left;
            if (verticalAlignment == VerticalAlignment.Stretch && inkSize.Height > clientSize.Height)
                verticalAlignment = VerticalAlignment.Top;
            vector.X = horizontalAlignment == HorizontalAlignment.Center || horizontalAlignment == HorizontalAlignment.Stretch
                ? (clientSize.Width - inkSize.Width) * 0.5
                : (horizontalAlignment != HorizontalAlignment.Right ? 0.0 : clientSize.Width - inkSize.Width);
            vector.Y = verticalAlignment == VerticalAlignment.Center || verticalAlignment == VerticalAlignment.Stretch
                ? (clientSize.Height - inkSize.Height) * 0.5
                : (verticalAlignment != VerticalAlignment.Bottom ? 0.0 : clientSize.Height - inkSize.Height);
            return vector;
        }
        public void Render(DrawingContext dc, FrameworkRenderElementContext context)
        {
            RenderCore(dc, context);
        }
        protected virtual Geometry GetLayoutClip(Size layoutSlotSize, FrameworkRenderElementContext context)
        {
            bool useLayoutRounding = UseLayoutRounding;
            bool needsClipBounds = context.NeedsClipBounds || ContentSpecificClipToBounds;
            bool clipToBounds = ClipToBounds;
            if (needsClipBounds || clipToBounds)
            {
                MinMax mm = new MinMax(this);
                Size inkSize = context.RenderSize;
                double maxWidthClip = (Double.IsPositiveInfinity(mm.MaxWidth) ? inkSize.Width : mm.MaxWidth);
                double maxHeightClip = (Double.IsPositiveInfinity(mm.MaxHeight) ? inkSize.Height : mm.MaxHeight);
                bool needToClipLocally = clipToBounds || maxWidthClip.LessThan(inkSize.Width) || maxHeightClip.LessThan(inkSize.Height);
                inkSize.Width = Math.Min(inkSize.Width, mm.MaxWidth);
                inkSize.Height = Math.Min(inkSize.Height, mm.MaxHeight);
                Rect inkRectTransformed = new Rect();
                Thickness margin = CalcDpiAwareThickness(context.Margin ?? Margin);
                double marginWidth = margin.Left + margin.Right;
                double marginHeight = margin.Top + margin.Bottom;
                Size clippingSize = new Size(Math.Max(0, layoutSlotSize.Width - marginWidth), Math.Max(0, layoutSlotSize.Height - marginHeight));
                bool needToClipSlot = clipToBounds || clippingSize.Width.LessThan(inkSize.Width) || clippingSize.Height.LessThan(inkSize.Height) || CalcNeedToClipSlot(inkSize, context);
                Transform rtlMirror = GetFlowDirectionTransform(context);
                if (needToClipLocally && !needToClipSlot)
                {
                    Rect clipRect = new Rect(0, 0, maxWidthClip, maxHeightClip);
                    if (useLayoutRounding)
                        clipRect = RoundLayoutRect(clipRect, DpiScaleX, DpiScaleY);
                    RectangleGeometry localClip = new RectangleGeometry(clipRect);
                    if (rtlMirror != null)
                        localClip.Transform = rtlMirror;
                    return localClip;
                }
                if (needToClipSlot)
                {
                    HorizontalAlignment horizontalAlignment = context.HorizontalAlignment ?? HorizontalAlignment;
                    VerticalAlignment verticalAlignment = context.VerticalAlignment ?? VerticalAlignment;
                    Vector offset = ComputeAlignmentOffset(clippingSize, inkSize, horizontalAlignment, verticalAlignment);
                    Rect slotRect = new Rect(-offset.X + inkRectTransformed.X, -offset.Y + inkRectTransformed.Y, clippingSize.Width, clippingSize.Height);
                    if (useLayoutRounding)
                        slotRect = RoundLayoutRect(slotRect, DpiScaleX, DpiScaleY);
                    if (needToClipLocally)
                    {
                        Rect localRect = new Rect(0, 0, maxWidthClip, maxHeightClip);
                        if (useLayoutRounding)
                        {
                            localRect = RoundLayoutRect(localRect, DpiScaleX, DpiScaleY);
                        }
                        slotRect.Intersect(localRect);
                    }
                    RectangleGeometry combinedClip = new RectangleGeometry(slotRect);
                    if (rtlMirror != null)
                        combinedClip.Transform = rtlMirror;
                    return combinedClip;
                }
                return null;
            }
            return GetLayoutClipBase(layoutSlotSize, context);
        }
        protected virtual bool CalcNeedToClipSlot(Size inkSize, FrameworkRenderElementContext context)
        {
            return false;
        }
        protected virtual Geometry GetLayoutClipBase(Size layoutSlotSize, FrameworkRenderElementContext context)
        {
            if (ClipToBounds)
            {
                RectangleGeometry rect = new RectangleGeometry(new Rect(context.RenderSize));
                rect.Freeze();
                return rect;
            }
            return null;
        }
        Transform GetFlowDirectionTransform(FrameworkRenderElementContext context)
        {
            if (ShouldApplyMirrorTransform(context))
                return new MatrixTransform(-1.0, 0.0, 0.0, 1.0, context.RenderSize.Width, 0.0);
            return null;
        }
        bool ShouldApplyMirrorTransform(FrameworkRenderElementContext context)
        {
            FlowDirection thisFlowDirection = context.FlowDirection ?? FlowDirection ?? System.Windows.FlowDirection.LeftToRight;
            FlowDirection parentFlowDirection = System.Windows.FlowDirection.LeftToRight;
            if (context.Parent != null)
                parentFlowDirection = context.Parent.FlowDirection ?? context.Parent.Factory.FlowDirection ?? System.Windows.FlowDirection.LeftToRight;
            return ApplyMirrorTransform(parentFlowDirection, thisFlowDirection);
        }
        bool ApplyMirrorTransform(FlowDirection parentFD, FlowDirection thisFD)
        {
            return ((parentFD == System.Windows.FlowDirection.LeftToRight && thisFD == System.Windows.FlowDirection.RightToLeft) ||
                    (parentFD == System.Windows.FlowDirection.RightToLeft && thisFD == System.Windows.FlowDirection.LeftToRight));
        }
        void RenderCore(DrawingContext dc, FrameworkRenderElementContext context)
        {
            var visibility = CalcRenderVisibility(context);
            if (visibility == Visibility.Collapsed)
                return;
            IFrameworkRenderElementContext c = context;
            bool shouldUseTransform = context.ShouldUseTransform();
            if (shouldUseTransform)
            {
                context.UpdateRenderTransform();
                dc.PushTransform(context.RenderTransform);
            }
            bool shouldUseLayoutClip = ShouldUseLayoutClip(context);
            if (shouldUseLayoutClip)
                dc.PushClip(context.LayoutClip);
            double opacity = context.ActualOpacity;
            bool shouldUseOpacity = visibility == Visibility.Visible && !opacity.AreClose(1d);
            if (shouldUseOpacity)
                dc.PushOpacity(opacity);
            if (visibility == Visibility.Visible)
                RenderOverride(dc, context);
            for (int i = 0; i < c.RenderChildrenCount; i++)
            {
                var child = c.GetRenderChild(i);
                child.Render(dc);
            }
            if (shouldUseOpacity)
                dc.Pop();
            if (shouldUseLayoutClip)
                dc.Pop();
            if (shouldUseTransform)
                dc.Pop();
        }
        bool ShouldUseLayoutClip(FrameworkRenderElementContext context)
        {
            return (context.NeedsClipBounds || ClipToBounds) && context.LayoutClip != null;
        }
        Visibility CalcRenderVisibility(FrameworkRenderElementContext context)
        {
            Visibility visibility = CalcParentVisibility(context);
            Visibility currentVisibility = CalcVisibility(context);
            if (visibility == Visibility.Visible)
                return currentVisibility;
            if (visibility == Visibility.Hidden)
                return currentVisibility == Visibility.Visible ? Visibility.Hidden : currentVisibility;
            return currentVisibility;
        }
        Visibility CalcParentVisibility(FrameworkRenderElementContext context)
        {
            FrameworkRenderElementContext parent = context.Parent;
            while (parent != null)
            {
                var visibility = CalcVisibility(parent);
                if (visibility != Visibility.Visible)
                    return visibility;
                parent = parent.Parent;
            }
            return Visibility.Visible;
        }
        Visibility CalcVisibility(FrameworkRenderElementContext context)
        {
            return context.Visibility ?? context.Factory.Visibility;
        }
        protected virtual Size MeasureOverride(Size availableSize, IFrameworkRenderElementContext context)
        {
            return availableSize;
        }
        protected virtual Size ArrangeOverride(Size finalSize, IFrameworkRenderElementContext context)
        {
            return finalSize;
        }
        protected virtual void RenderOverride(DrawingContext dc, IFrameworkRenderElementContext context)
        {
            var frec = (FrameworkRenderElementContext)context;
            var showBounds = frec.ShowBounds ?? ShowBounds;
            if (showBounds)
                dc.DrawRectangle(null, new Pen(Brushes.Red, 1d), new Rect(new Point(0, 0), context.RenderSize));
        }
        protected internal FrameworkRenderElementContext CreateContext(INamescope namescope, IElementHost elementHost)
        {
            FreezeIfNeeded();
            var context = CreateContextInstance();
            context.Namescope = namescope;
            context.ElementHost = elementHost;
            context.Foreground = Foreground;
            context.FlowDirection = FlowDirection;
            InitializeContext(context);
            namescope.RegisterElement(context);
            return context;
        }
        void FreezeIfNeeded()
        {
            if (isFreezed)
                return;
            lock (locker)
            {
                if (isFreezed)
                    return;
                Name = string.IsNullOrEmpty(Name) ? Guid.NewGuid().ToString() : Name;
                isFreezed = true;
            }
        }
        protected virtual FrameworkRenderElementContext CreateContextInstance()
        {
            return new FrameworkRenderElementContext(this);
        }
        protected virtual void InitializeContext(FrameworkRenderElementContext context)
        {
        }
        protected void SetProperty<T>(ref T container, T value)
        {
            if (isFreezed)
                throw new ArgumentException("already frozen");
            if (object.Equals(container, value))
                return;
            container = value;
        }
    }
}
